package mviewer.renderer.jogl;

import com.jogamp.opengl.util.texture.Texture;
import com.jogamp.opengl.util.texture.TextureIO;
import java.io.File;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
import java.util.logging.Logger;
import javax.media.opengl.GL2;
import javax.media.opengl.GLAutoDrawable;
import javax.media.opengl.GLEventListener;
import javax.media.opengl.fixedfunc.GLMatrixFunc;
import javax.media.opengl.glu.GLU;
import mviewer.renderer.IRenderer;
import mviewer.types.Camera;
import mviewer.types.IModel3d;
import mviewer.types.IVector;
import mviewer.types.Material;
import mviewer.types.VectorData;

public class SceneController implements GLEventListener {

	static private final int WORLD_SIZE = 40;

	static private final Logger logger = Logger.getLogger(SceneController.class.getName());

	private GLU glu;
	private Camera camera;
	private IModel3d model;
	private Map<Material, Texture> textures;
	private boolean resetTextures = false;
	private float worldScaling = 1;
	private IVector modelCenter;

	public IModel3d getModel() {
		return model;
	}

	public void setModel(IModel3d model) {
		float modelSize = model.calculateDiagonalLength();
		this.model = model;
		worldScaling = WORLD_SIZE / modelSize;
		modelCenter = model.calculateCenter();
		modelCenter = new VectorData(modelCenter.getX() * worldScaling,
									modelCenter.getY() * worldScaling,
									modelCenter.getZ() * worldScaling);
		resetTextures = true;
	}

	public void setCamera(Camera camera) {
		this.camera = camera;
	}

	@Override
	public void init(GLAutoDrawable drawable) {
		GL2 gl = drawable.getGL().getGL2();
		// create utilities
		glu = GLU.createGLU();

		// set background color in RGBA. Alpha: 0 (transparent) 1 (opaque)
		gl.glClearColor(IRenderer.BACKGROUND.getX(),
						IRenderer.BACKGROUND.getY(),
						IRenderer.BACKGROUND.getZ(), 0);
		// setup the depth buffer and enable the depth testing
		gl.glClearDepth(1.0f);         // clear z-buffer to the farthest
		gl.glDepthFunc(GL2.GL_LEQUAL);  // the type of depth test to do
		gl.glEnable(GL2.GL_DEPTH_TEST); // enables depth testing
		// setup the best perspective correction
		gl.glHint(GL2.GL_PERSPECTIVE_CORRECTION_HINT, GL2.GL_NICEST);
		// setup polygon draw mode
		gl.glPolygonMode(GL2.GL_FRONT, GL2.GL_FILL);
		gl.glPolygonMode(GL2.GL_BACK,  GL2.GL_LINE);
		// setup lighting
		gl.glEnable(GL2.GL_LIGHTING);
		gl.glEnable(GL2.GL_LIGHT0);
		gl.glEnable(GL2.GL_NORMALIZE);
		gl.glEnable(GL2.GL_BLEND);
		gl.glBlendFunc(GL2.GL_SRC_ALPHA, GL2.GL_ONE_MINUS_SRC_ALPHA);
		setupLighting(gl);
		// setup texture behaviour
		gl.glTexParameteri(GL2.GL_TEXTURE_2D, GL2.GL_GENERATE_MIPMAP, GL2.GL_TRUE);
		gl.glTexParameteri(GL2.GL_TEXTURE_2D, GL2.GL_TEXTURE_MIN_FILTER, GL2.GL_NEAREST_MIPMAP_LINEAR);
	}

	@Override
	public void dispose(GLAutoDrawable drawable) {
		if (textures != null) {
			GL2 gl = drawable.getGL().getGL2();

			unloadTextures(gl);
		}
	}

	@Override
	public void reshape(GLAutoDrawable drawable, int x, int y, int width, int height) {
		GL2 gl = drawable.getGL().getGL2();

		setupProjection(gl, x, y, width, height);

		gl.glMatrixMode(GLMatrixFunc.GL_MODELVIEW);
		// reset matrix
		gl.glLoadIdentity();
		// set camera (position, focus, orientation)
		IVector p = camera.getPosition(),
				f = camera.getFocus(),
				o = camera.getOrientation();
		glu.gluLookAt(p.getX(), p.getY(), p.getZ(),
					  f.getX(), f.getY(), f.getZ(),
					  o.getX(), o.getY(), o.getZ());
	}

	@Override
	public void display(GLAutoDrawable drawable) {
		GL2 gl = drawable.getGL().getGL2();

		gl.glClear(GL2.GL_COLOR_BUFFER_BIT | GL2.GL_DEPTH_BUFFER_BIT);

		if (model != null) {
			if (resetTextures) {
				loadTextures(gl);
				resetTextures = false;
			}
			gl.glPushMatrix();
			gl.glTranslatef(-modelCenter.getX(), -modelCenter.getY(), -modelCenter.getZ());
			gl.glScalef(worldScaling, worldScaling, worldScaling);
			model.draw(new DrawerContext(gl, textures));
			gl.glPopMatrix();
		}

		gl.glFlush();
	}

	private void setupProjection(GL2 gl, int x, int y, int width, int height) {
		// Set viewport to cover full screen
		gl.glViewport(x, y, width, height);

		// Set up the projection matrix - choose perspective view
		gl.glMatrixMode(GLMatrixFunc.GL_PROJECTION);
		gl.glLoadIdentity(); // reset
		// setup projection frustrum as pyramid with an angle of 45 degrees
		float aspect = height==0 ? 1f : width / (float)height;
		glu.gluPerspective(45.0f, aspect, camera.getZNear(), camera.getZFar());
	}

	private void setupLighting(GL2 gl) {
		IVector p = camera.getPosition();
		float[] position = new float[] {p.getX(), p.getY() + 5, p.getZ(), 0};
		float[] ambient  = new float[] {1, 1, 1, 1};
		float[] diffuse  = new float[] {1, 1, 1, 1};
		float[] specular = new float[] {1, 1, 1, 1};

		gl.glLightModeli(GL2.GL_LIGHT_MODEL_LOCAL_VIEWER, GL2.GL_TRUE);
		if (gl.isExtensionAvailable("GL_VERSION_1_2"))
			gl.glLightModeli(GL2.GL_LIGHT_MODEL_COLOR_CONTROL, GL2.GL_SEPARATE_SPECULAR_COLOR);
		gl.glLightfv(GL2.GL_LIGHT0, GL2.GL_POSITION, position, 0);
		gl.glLightfv(GL2.GL_LIGHT0, GL2.GL_AMBIENT, ambient, 0);
		gl.glLightfv(GL2.GL_LIGHT0, GL2.GL_DIFFUSE, diffuse, 0);
		gl.glLightfv(GL2.GL_LIGHT0, GL2.GL_SPECULAR, specular, 0);
		/*gl.glLightf(GL2.GL_LIGHT0, GL2.GL_CONSTANT_ATTENUATION, 1f);
		gl.glLightf(  GL2.GL_LIGHT0, GL2.GL_LINEAR_ATTENUATION, 0f);
		gl.glLightf(  GL2.GL_LIGHT0, GL2.GL_QUADRATIC_ATTENUATION, 0f);*/
	}

	private void loadTextures(GL2 gl) {
		if (textures != null)
			unloadTextures(gl);

		textures = new HashMap<Material, Texture>();
		Set<Material> materials = new HashSet<Material>();

		model.listContainedMaterials(materials);

		for (Material material : materials) {
			String textureFile = material.getTextureFile();
			if (textureFile != null) {
				try {
					Texture texture = TextureIO.newTexture(new File(textureFile), true);
					textures.put(material, texture);
				} catch(Exception e) {
					logger.warning(String.format("Couldn't read texture file: %s, Error: %s", textureFile, e.getMessage()));
				}
			}
		}

		logger.info(String.format("%d material(s) found: %s\n%d texture(s) loaded: %s",
			materials.size(), materials, textures.size(), textures.keySet()));
    }

	private void unloadTextures(GL2 gl) {
		for (Texture texture : textures.values())
			texture.destroy(gl);
		textures = null;
	}
}